package xp.ass.imitate.express.impl;

import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.lang.Nullable;
import xp.ass.imitate.express.XExpression;
import xp.ass.imitate.express.XExpressionParser;
import xp.ass.imitate.express.XEvaluateContext;
import xp.ass.imitate.express.meta.node.CompoundTokenNode;
import xp.ass.imitate.express.meta.node.FunctionTokenNode;
import xp.ass.imitate.express.meta.node.LiteralTokenNode;
import xp.ass.imitate.express.meta.node.PropertyTokenNode;
import xp.ass.imitate.express.meta.node.VariableTokenNode;
import xp.ass.imitate.express.meta.XToken;
import xp.ass.imitate.express.meta.XTokenKind;
import xp.ass.imitate.express.meta.node.XTokenNode;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;

/**
 * @author chent3
 * @date 2024/8/23
 */
public class XExpressionParserImpl implements XExpressionParser {

    //  for collection or  map, we`d better given a default value to avoid npe
    private List<XToken> tokenStream;
    private int tokenStreamLen;
    private int tokenStreamPointer;
    private String express;
    private Deque<XTokenNode> tokenNodeQueue = new ArrayDeque<>();

    @Override
    public XExpression parse(String expression, XEvaluateContext context) {
        this.express = expression;

        XTokenizer xTokenizer = new XTokenizer(expression);
        this.tokenStream = xTokenizer.process();
        this.tokenStreamLen = this.tokenStream.size();
        this.tokenStreamPointer = 0;

        XTokenNode ast = eatFunctionOrVariableExpression();
        return new XExpressionImpl(expression, ast);
    }

    public XTokenNode eatFunctionOrVariableExpression() {
        XTokenNode startNode = eatStartNode();
        List<XTokenNode> nodeList = Lists.newArrayList(startNode);
        XTokenNode node = eatDotNode();
        while (node != null) {
            nodeList.add(node);
            node = eatDotNode();
        }
        if (startNode == null || CollectionUtils.isEmpty(nodeList)) {
            return startNode;
        }
        return new CompoundTokenNode(nodeList, compressPos(startNode.getStartPos(), nodeList.get(nodeList.size() - 1).getEndPos()));
    }

    @Nullable
    private XTokenNode eatDotNode() {
        if (!peekToken(XTokenKind.DOT)) {
            return null;
        }
        XToken dotToken = takeToken();
        XToken propertyName = takeToken();
        XTokenNode[] args = mayEatMethodArgs();
        if (args == null) {
            this.tokenNodeQueue.push(new PropertyTokenNode(propertyName.getData(this.express), compressPos(propertyName.getStartPos(), propertyName.getEndPos())));
        } else {
            this.tokenNodeQueue.push(new FunctionTokenNode(propertyName.getData(this.express), args, compressPos(propertyName.getStartPos(), propertyName.getEndPos())));
        }
        return this.tokenNodeQueue.pop();
    }

    public XTokenNode eatStartNode() {
        // parse literal
        if (maybeEatLiteral()) {
            return tokenNodeQueue.pop();
        }

        if (maybeEatFunctionOrVariable()) {
            return tokenNodeQueue.pop();
        }


        return null;
    }

    private boolean maybeEatLiteral() {
        if (!peekToken(XTokenKind.IDENTIFER)) {
            return false;
        }
        XToken token = takeToken();
        this.tokenNodeQueue.push(new LiteralTokenNode(token.getData(this.express), compressPos(token.getStartPos(), token.getEndPos())));
        return true;
    }


    // traverse express
    //  chose branch by judge each token , eg: parse #user.name , just need to judge # identify
    // variable express => #user.name, method invoke express => #queryUserName(2);
    private boolean maybeEatFunctionOrVariable() {
        if (!peekToken(XTokenKind.HASH)) {
            return false;
        }
        // obtain # flag
        XToken flatToken = takeToken();
        // obtain literal
        XToken functionOrVariableName = eatToken(XTokenKind.IDENTIFER);
        // whether method invoke or variable express
        // iftk_todo later we implement method invoke express
        XTokenNode[] args = mayEatMethodArgs();

        // variable express parse branch
        if (args == null) {
            tokenNodeQueue.push(new VariableTokenNode(functionOrVariableName.getData(this.express), compressPos(flatToken.getStartPos(), functionOrVariableName.getEndPos())));
        } else {
            tokenNodeQueue.push(new FunctionTokenNode(functionOrVariableName.getData(this.express), args, compressPos(flatToken.getStartPos(), functionOrVariableName.getEndPos())));
        }
        return true;
    }

    @Nullable
    private XTokenNode[] mayEatMethodArgs() {
        // If the expression does not contain "(", it must be a variable expression
        if (!peekToken(XTokenKind.LPAREN)) {
            return null;
        }


        return null;
    }

    private XToken eatToken(XTokenKind xTokenKind) {
        XToken t = nextToken();
        if (t == null) {
            throw new IllegalArgumentException("can`t find token");
        }
        if (t.getXTokenKind() != xTokenKind) {
            throw new IllegalArgumentException("token not match");
        }
        return t;
    }

    private XToken takeToken() {
        if (this.tokenStreamPointer >= this.tokenStreamLen) {
            throw new IllegalArgumentException("No token");
        }
        return this.tokenStream.get(this.tokenStreamPointer++);
    }

    private boolean peekToken(XTokenKind desireTokenKind) {
        return peekToken(desireTokenKind, Boolean.FALSE);
    }

    private boolean peekToken(XTokenKind desireTokenKind, boolean consumeIfMatched) {
        XToken token = peekToken();
        if (token == null) {
            return false;
        }
        if (token.getXTokenKind() == desireTokenKind) {
            if (consumeIfMatched) {
                this.tokenStreamPointer++;
            }
            return true;
        }
        return false;
    }

    @Nullable
    private XToken peekToken() {
        if (this.tokenStreamPointer >= this.tokenStreamLen) {
            return null;
        }
        return this.tokenStream.get(this.tokenStreamPointer);
    }

    @Nullable
    private XToken nextToken() {
        if (this.tokenStreamPointer >= this.tokenStreamLen) {
            return null;
        }
        return this.tokenStream.get(this.tokenStreamPointer++);
    }


    public int compressPos(int start, int end) {
        return (start << 16) + end;
    }
}
